<?php
/**
 * Copyright (c) 2006-2007, Julien PORTALIER
 * 
 * Licensed under The LGPL License
 * Redistributions of files must retain the above copyright notice.
 * 
 * @copyright    Copyright (c) 2006-2007, Julien PORTALIER
 * @link         http://feather-cm.googlecode.com/
 * @license      http://www.opensource.org/licenses/lgpl-license.php The LGPL License
 */

abstract class GenericTagModel extends ActiveRecord
{
	protected $refererClass;
	public    $recursiveProcess = Q_PROCESS_LIST;
	public    $recursiveFields  = 'tag';
	public    $tagName          = 'tag';
	
	// magic methods
	
	function __get($class)
	{
		if ($class == 'referer')
		{
			if (empty($this->refererClass))
				trigger_error("{$this->name}::refererClass must be defined.", E_USER_ERROR);
			return $this->referer = $this->{$this->refererClass};
		}
		return parent::__get($class);
	}
	
	// read
	
	/**
	 * Searches for a tag.
	 * 
	 * @return array A list of tags (may be empty).
	 */
	function & search($q)
	{
		$this->process = Q_PROCESS_LIST;
		$this->condition('tag', "LIKE $q%");
		return $this->findAll('DISTINCT tag', null, null, -1);
	}
	
	/**
	 * Returns a tagcloud.
	 * 
	 * @return array A tagcloud ready array of tags.
	 * @param  integer $limit Return only the n most used tags (optional).
	 * @param  integer $min_occurrences
	 */
	function & getCloud($limit=null, $min_occurrences=null)
	{
		# prepare query
		$fields = array(
			"{$this->name}.{$this->tagName}",
			'-! COUNT( '.$this->db->field("{$this->referer->name}.{$this->referer->primaryKey}").' ) AS '.$this->db->field('tagcounter')
		);
		
		if ($min_occurrences) {
			$this->groupby("-! GROUP BY {$this->name}.{$this->tagName} HAVING tagcounter >= {$min_occurrences}");
		}
		else {
			$this->groupby("{$this->name}.{$this->tagName}");
		}
		$this->order('tagcounter DESC');
		
		# query
		$this->process = Q_PROCESS_LITE;
		$_tags = $this->findAll(&$fields, $limit, null, 0);
		
		# generate a tag cloud ready array
		$tags = array();
		foreach ($_tags as $tag)
			$tags[$tag[$this->tagName]] = $tag['tagcounter'];
		
		# sort tags by name
		ksort($tags, SORT_LOCALE_STRING);
		
		return $tags;
	}
	
	/**
	 * Returns a tagcloud ready array of related tags of one or more tags.
	 * 
	 * @return array Tagcloud ready array of tags.
	 * @param  mixed $tags List fo tags, either as array or comma separated string.
	 */
	function & getRelated($tags, $limit=null)
	{
		if (!is_array($tags))
			$tags = explode(',', $tags);
		
		$this->conditions(array(
			"{$this->referer->name}.{$this->referer->primaryKey}" => "-! IN ( ".$this->__getSQL($tags)." )",
			"{$this->name}.{$this->tagName}" => array('NOT' => &$tags)
		));
		
		return $this->getCloud($limit);
	}
	
	/**
	 * Generates an SQL query a search on multiple tags.
	 * 
	 * @return string SQL query.
	 * @param  mixed A list of tags, either as array or comma separated string.
	 */
	protected function __getSQL($tags)
	{
		if (!is_array($tags))
			$tags = explode(',', $tags);
		$referer = $this->referer;
		$db = $this->db;
		
		# prepare SQL query
		$field = $db->field("{$referer->name}.{$referer->primaryKey}");
		$base  = "SELECT $field ".
			" FROM {$referer->table} AS ".$db->field($referer->name).
			" LEFT JOIN {$this->table} AS ".$db->field($this->name).
			" ON ".$db->field("{$this->name}.{$referer->foreignKey}")." = $field".
			" WHERE ".$db->field("{$this->name}.{$this->tagName}")." = %s";
		
		# create SQL query
		$sql = sprintf($base, $db->value(array_shift($tags)));
		$end = '';
		foreach($tags as $tag)
		{
			$tag = trim($tag);
			if (!empty($tag))
			{
				$sql .= " AND $field IN ( ".sprintf($base, $db->value($tag));
				$end .= ' )';
			}
		}
		
		return $sql.$end;
	}
	
	/**
	 * Sets conditions on referer to find instances that share common tag(s). 
	 */
	function setConditions($tags)
	{
		if (!is_array($tags))
			$tags = explode(',', $tags);
		
		if (count($tags) == 1)
		{
			# single tag
			$this->referer->conditions(array("{$this->name}.{$this->tagName}" => $tags[0]));
		}
		else
		{
			# multi-tags
			$this->referer->conditions(array("{$this->referer->name}.{$this->referer->primaryKey}" =>
				"-! IN ( ".$this->__getSQL(&$tags)." )"));
			$this->referer->unbind(array('hasOne' => $this->name));
		}
	}
	
	// create & update
	
	protected function __save($type=null)
	{
		# get tags
		$tags = $this->data[$this->name][Inflector::pluralize($this->tagName)];
		
		# erase all tags?
		if (empty($tags))
		{
			$this->condition($this->referer->foreignKey, $this->referer->id);
			$this->__delete();
			return true;
		}
		
		# clean tags, remove duplicates, etc.
		if (is_string($tags))
			$tags = split('[,;]', $tags);
		
		$tags = array_unique($tags);
		foreach($tags as $i => $tag)
		{
			$tag = trim($tag);
			if (!empty($tag))
				$tags[$i] = $tag;
			else
				unset($tags[$i]);
		}
		
		# already sticked tags
		$this->condition($this->referer->foreignKey, $this->referer->id);
		$_tags = $this->listField($this->tagName, -1);
		
		$new_tags = array_diff($tags, $_tags);
		$del_tags = array_diff($_tags, $tags);
		
		# remove erased tags
		foreach($del_tags as $tag)
		{
			$this->conditions(array(
				$this->referer->foreignKey => $this->referer->id,
				$this->tagName => $tag
			));
			if (!$this->__delete())
				return false;
		}
		
		# add new tags
		if (!empty($new_tags))
		{
			$values = array();
			foreach($new_tags as $tag)
			{
				$entry = array($this->referer->id, $tag);
				array_push($values, $entry);
			}
			if (!$this->saveMany(array($this->referer->foreignKey, $this->tagName), &$values))
				return false;
		}
		
		return $this->afterSave();
	}
	
	function rename($old, $new)
	{
		$this->condition($this->tagName, $old);
		return $this->saveField($this->tagName, $new);
	}
}
?>
